/*  Starshatter OpenSource Distribution
    Copyright (c) 1997-2004, Destroyer Studios LLC.
    All Rights Reserved.

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice,
      this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice,
      this list of conditions and the following disclaimer in the documentation
      and/or other materials provided with the distribution.
    * Neither the name "Destroyer Studios" nor the names of its contributors
      may be used to endorse or promote products derived from this software
      without specific prior written permission.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
    LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    POSSIBILITY OF SUCH DAMAGE.

    SUBSYSTEM:    nGenEx.lib
    FILE:         Physical.cpp
    AUTHOR:       John DiCamillo


    OVERVIEW
    ========
    Abstract Physical Object
*/

#include "MemDebug.h"
#include "Physical.h"
#include "Graphic.h"
#include "Light.h"
#include "Director.h"

// +--------------------------------------------------------------------+

int      Physical::id_key     = 1;
double   Physical::sub_frame  = 1.0 / 60.0;

static const double GRAV = 6.673e-11;

// +--------------------------------------------------------------------+

Physical::Physical()
    : id(id_key++), obj_type(0), rep(0), light(0),
      thrust(0.0f), drag(0.0f), lat_thrust(false),
      trans_x(0.0f), trans_y(0.0f), trans_z(0.0f), straight(false),
      roll(0.0f), pitch(0.0f), yaw(0.0f), dr(0.0f), dp(0.0f), dy(0.0f),
      dr_acc(0.0f), dp_acc(0.0f), dy_acc(0.0f),
      dr_drg(0.0f), dp_drg(0.0f), dy_drg(0.0f),
      flight_path_yaw(0.0f), flight_path_pitch(0.0f), primary_mass(0),
      roll_rate(1.0f), pitch_rate(1.0f), yaw_rate(1.0f), shake(0.0f),
      radius(0.0f), mass(1.0f), integrity(1.0f), life(-1), dir(0),
      g_accel(0.0f), Do(0.0f), CL(0.0f), CD(0.0f), alpha(0.0f), stall(0.0f)
{
    strcpy_s(name, "unknown object");
}

// +--------------------------------------------------------------------+

Physical::Physical(const char* n, int t)
    : id(id_key++), obj_type(t), rep(0), light(0),
      thrust(0.0f), drag(0.0f), lat_thrust(false),
      trans_x(0.0f), trans_y(0.0f), trans_z(0.0f), straight(false),
      roll(0.0f), pitch(0.0f), yaw(0.0f), dr(0.0f), dp(0.0f), dy(0.0f),
      dr_acc(0.0f), dp_acc(0.0f), dy_acc(0.0f),
      dr_drg(0.0f), dp_drg(0.0f), dy_drg(0.0f),
      flight_path_yaw(0.0f), flight_path_pitch(0.0f), primary_mass(0),
      roll_rate(1.0f), pitch_rate(1.0f), yaw_rate(1.0f), shake(0.0f),
      radius(0.0f), mass(1.0f), integrity(1.0f), life(-1), dir(0),
      g_accel(0.0f), Do(0.0f), CL(0.0f), CD(0.0f), alpha(0.0f), stall(0.0f)
{
    strncpy_s(name, n, NAMELEN-1);
    name[NAMELEN-1] = 0;
}

// +--------------------------------------------------------------------+

Physical::~Physical()
{
    // inform graphic rep and light that we are leaving:
    GRAPHIC_DESTROY(rep);
    LIGHT_DESTROY(light);

    // we own the director
    delete dir;
    dir = 0;
}

// +--------------------------------------------------------------------+

inline double random() { return rand()-16384; }

void
Physical::ExecFrame(double s)
{
    Point orig_velocity = Velocity();
    arcade_velocity = Point();

    // if this object is under direction,
    // but doesn't need subframe accuracy,
    // update the control parameters:
    if (dir && !dir->Subframe())
        dir->ExecFrame(s);

    // decrement life before destroying the frame time:
    if (life > 0)
        life -= s;

    // integrate equations
    // using slices no larger
    // than sub_frame:

    double seconds = s;

    while (s > 0.0) {
        if (s > sub_frame)
        seconds = sub_frame;
        else
        seconds = s;

        // if the director needs subframe accuracy, run it now:
        if (dir && dir->Subframe())
        dir->ExecFrame(seconds);

        if (!straight)
        AngularFrame(seconds);

        // LINEAR MOVEMENT ----------------------------
        Point pos = cam.Pos();

        // if the object is thrusting,
        // accelerate along the camera normal:
        if (thrust) {
            Point thrustvec = cam.vpn();
            thrustvec *= ((thrust/mass) * seconds);
            velocity += thrustvec;
        }

        LinearFrame(seconds);

        // move the position by the (time-frame scaled) velocity:
        pos += velocity * seconds;
        cam.MoveTo(pos);

        s -= seconds;
    }

    alpha = 0.0f;

    // now update the graphic rep and light sources:
    if (rep) {
        rep->MoveTo(cam.Pos());
        rep->SetOrientation(cam.Orientation());
    }

    if (light) {
        light->MoveTo(cam.Pos());
    }

    if (!straight)
        CalcFlightPath();

    accel = (Velocity() - orig_velocity) * (1/seconds);
    if (!_finite(accel.x) || !_finite(accel.y) || !_finite(accel.z))
        accel = Point();
}

// +--------------------------------------------------------------------+

void
Physical::AeroFrame(double s)
{
    arcade_velocity = Point();

    // if this object is under direction,
    // but doesn't need subframe accuracy,
    // update the control parameters:
    if (dir && !dir->Subframe())
        dir->ExecFrame(s);

    // decrement life before destroying the frame time:
    if (life > 0)
        life -= s;

    // integrate equations
    // using slices no larger
    // than sub_frame:

    double seconds = s;

    while (s > 0.0) {
        if (s > sub_frame)
        seconds = sub_frame;
        else
        seconds = s;

        // if the director needs subframe accuracy, run it now:
        if (dir && dir->Subframe())
        dir->ExecFrame(seconds);

        AngularFrame(seconds);

        // LINEAR MOVEMENT ----------------------------
        Point pos = cam.Pos();

        // if the object is thrusting,
        // accelerate along the camera normal:
        if (thrust) {
            Point thrustvec = cam.vpn();
            thrustvec *= ((thrust/mass) * seconds);
            velocity += thrustvec;
        }

        // AERODYNAMICS ------------------------------

        if (lat_thrust)
            LinearFrame(seconds);

        // if no thrusters, do constant gravity:
        else if (g_accel > 0)
            velocity += Point(0, -g_accel, 0) * seconds;

        // compute alpha, rho, drag, and lift:

        Point  vfp     = velocity;
        double v       = vfp.Normalize();
        double v_2     = 0;
        double rho     = GetDensity();
        double lift    = 0;

        if (v > 150) {
            v_2 = (v-150) * (v-150);

            Point  vfp1 = vfp - cam.vrt() * (vfp * cam.vrt());
            vfp1.Normalize();

            double cos_alpha = vfp1 * cam.vpn();

            if (cos_alpha >= 1) {
                alpha = 0.0f;
            }
            else {
                alpha = (float) acos(cos_alpha);
            }

            // if flight path is above nose, alpha is negative:
            if (vfp1 * cam.vup() > 0)
            alpha = -alpha;

            if (alpha <= stall) {
                lift = CL * alpha * rho * v_2;
            }
            else {
                lift = CL * (2*stall - alpha) * rho * v_2;
            }

            // add lift to velocity:
            if (_finite(lift))
            velocity += cam.vup() * lift * seconds;
            else
            lift = 0;

            // if drag applies, decellerate:
            double alpha_2 = alpha*alpha;
            double drag_eff = (drag + (CD * alpha_2)) * rho * v_2;

            Point vn = velocity;
            vn.Normalize();

            velocity += vn * -drag_eff * seconds;
        }
        else {
            velocity *= exp(-drag * seconds);
        }

        // move the position by the (time-frame scaled) velocity:
        pos += velocity * seconds;
        cam.MoveTo(pos);

        s -= seconds;
    }

    // now update the graphic rep and light sources:
    if (rep) {
        rep->MoveTo(cam.Pos());
        rep->SetOrientation(cam.Orientation());
    }

    if (light) {
        light->MoveTo(cam.Pos());
    }
}

double
Physical::GetDensity() const
{
    double alt = cam.Pos().y;
    double rho = 0.75 * Do * (250e3-alt)/250e3;

    return rho;
}

// +--------------------------------------------------------------------+

void
Physical::ArcadeFrame(double s)
{
    // if this object is under direction,
    // but doesn't need subframe accuracy,
    // update the control parameters:
    if (dir && !dir->Subframe())
        dir->ExecFrame(s);

    // decrement life before destroying the frame time:
    if (life > 0)
        life -= s;

    // integrate equations
    // using slices no larger
    // than sub_frame:

    double seconds = s;

    while (s > 0.0) {
        if (s > sub_frame)
        seconds = sub_frame;
        else
        seconds = s;

        // if the director needs subframe accuracy, run it now:
        if (dir && dir->Subframe())
            dir->ExecFrame(seconds);

        if (!straight)
            AngularFrame(seconds);

        Point pos = cam.Pos();

        // ARCADE FLIGHT MODEL:
        // arcade_velocity vector is always in line with heading

        double speed = arcade_velocity.Normalize();
        double bleed = arcade_velocity * cam.vpn();

        speed *= pow(bleed, 30);
        arcade_velocity = cam.vpn() * speed;

        if (thrust) {
            Point thrustvec = cam.vpn();
            thrustvec *= ((thrust/mass) * seconds);
            arcade_velocity += thrustvec;
        }

        if (drag)
            arcade_velocity *= exp(-drag * seconds);

        LinearFrame(seconds);

        // move the position by the (time-frame scaled) velocity:
        pos += arcade_velocity * seconds + 
        velocity        * seconds;

        cam.MoveTo(pos);

        s -= seconds;
    }

    alpha = 0.0f;

    // now update the graphic rep and light sources:
    if (rep) {
        rep->MoveTo(cam.Pos());
        rep->SetOrientation(cam.Orientation());
    }

    if (light) {
        light->MoveTo(cam.Pos());
    }
}

// +--------------------------------------------------------------------+

void
Physical::AngularFrame(double seconds)
{
    if (!straight) {
        dr += (float) (dr_acc * seconds);
        dy += (float) (dy_acc * seconds);
        dp += (float) (dp_acc * seconds);
        
        dr *= (float) exp(-dr_drg * seconds);
        dy *= (float) exp(-dy_drg * seconds);
        dp *= (float) exp(-dp_drg * seconds);

        roll  = (float) (dr * seconds);
        pitch = (float) (dp * seconds);
        yaw   = (float) (dy * seconds);

        if (shake > 0.01) {
            vibration = Point(random(), random(), random());
            vibration.Normalize();
            vibration *= (float) (shake * seconds);

            shake *= (float) exp(-1.5 * seconds);
        }
        else {
            vibration.x = vibration.y = vibration.z = 0.0f;
            shake = 0.0f;
        }

        cam.Aim(roll, pitch, yaw);
    }
}

// +--------------------------------------------------------------------+

void
Physical::LinearFrame(double seconds)
{
    // deal with lateral thrusters:

    if (trans_x) { // side-to-side
        Point transvec = cam.vrt();
        transvec *= ((trans_x/mass) * seconds);

        velocity += transvec;
    }

    if (trans_y) { // fore-and-aft
        Point transvec = cam.vpn();
        transvec *= ((trans_y/mass) * seconds);

        velocity += transvec;
    }

    if (trans_z) { // up-and-down
        Point transvec = cam.vup();
        transvec *= ((trans_z/mass) * seconds);

        velocity += transvec;
    }

    // if gravity applies, attract:
    if (primary_mass > 0) {
        Point  g = primary_loc - cam.Pos();
        double r = g.Normalize();

        g *= GRAV * primary_mass / (r*r);

        velocity += g * seconds;
    }

    // constant gravity:
    else if (g_accel > 0) {
        velocity += Point(0, -g_accel, 0) * seconds;
    }

    // if drag applies, decellerate:
    if (drag)
        velocity *= exp(-drag * seconds);
}

// +--------------------------------------------------------------------+

void
Physical::CalcFlightPath()
{
    flight_path_yaw   = 0.0f;
    flight_path_pitch = 0.0f;

    // transform flight path into camera frame:
    Point flight_path = velocity;
    if (flight_path.Normalize() < 1)
    return;

    Point tmp = flight_path;
    flight_path.x = tmp * cam.vrt();
    flight_path.y = tmp * cam.vup();
    flight_path.z = tmp * cam.vpn();

    if (flight_path.z < 0.1)
        return;

    // first, compute azimuth:
    flight_path_yaw = (float) atan(flight_path.x / flight_path.z);
    if (flight_path.z < 0)     flight_path_yaw -= (float) PI;
    if (flight_path_yaw < -PI) flight_path_yaw += (float) (2*PI);

    // then, rotate path into azimuth frame to compute elevation:
    Camera yaw_cam;
    yaw_cam.Clone(cam);
    yaw_cam.Yaw(flight_path_yaw);

    flight_path.x = tmp * yaw_cam.vrt();
    flight_path.y = tmp * yaw_cam.vup();
    flight_path.z = tmp * yaw_cam.vpn();

    flight_path_pitch = (float) atan(flight_path.y / flight_path.z);
}

// +--------------------------------------------------------------------+

void
Physical::MoveTo(const Point& new_loc)
{
    cam.MoveTo(new_loc);
}

void
Physical::TranslateBy(const Point& ref)
{
    Point new_loc = cam.Pos() - ref;
    cam.MoveTo(new_loc);
}

void
Physical::ApplyForce(const Point& force)
{
    velocity += force/mass;
}

void
Physical::ApplyTorque(const Point& torque)
{
    dr += (float) (torque.x/mass);
    dp += (float) (torque.y/mass);
    dy += (float) (torque.z/mass);
}

void
Physical::SetThrust(double t)
{
    thrust = (float) t;
}

void
Physical::SetTransX(double t)
{
    trans_x = (float) t;
}

void
Physical::SetTransY(double t)
{
    trans_y = (float) t;
}

void
Physical::SetTransZ(double t)
{
    trans_z = (float) t;
}

// +--------------------------------------------------------------------+

void
Physical::SetHeading(double r, double p, double y)
{
    roll  = (float) r;
    pitch = (float) p;
    yaw   = (float) y;

    cam.Aim(roll, pitch, yaw);
}

void
Physical::LookAt(const Point& dst)
{
    cam.LookAt(dst);
}

void
Physical::CloneCam(const Camera& c)
{
    cam.Clone(c);
}

void
Physical::SetAbsoluteOrientation(double r, double p, double y)
{
    roll  = (float) r;
    pitch = (float) p;
    yaw   = (float) y;

    Camera work(Location().x, Location().y, Location().z);
    work.Aim(r,p,y);
    cam.Clone(work);
}

void
Physical::ApplyRoll(double r)
{
    if (r > 1)       r =  1;
    else if (r < -1) r = -1;

    dr_acc = (float) r * roll_rate;
}

void
Physical::ApplyPitch(double p)
{
    if (p > 1)       p =  1;
    else if (p < -1) p = -1;

    dp_acc = (float) p * pitch_rate;
}

void
Physical::ApplyYaw(double y)
{
    if (y > 1)       y =  1;
    else if (y < -1) y = -1;

    dy_acc = (float) y * yaw_rate;
}

void
Physical::SetAngularRates(double  r, double  p, double  y)
{
    roll_rate  = (float) r;
    pitch_rate = (float) p;
    yaw_rate   = (float) y;
}

void
Physical::GetAngularRates(double& r, double& p, double& y)
{
    r = roll_rate;
    p = pitch_rate;
    y = yaw_rate;
}

void
Physical::SetAngularDrag(double  r, double  p, double  y)
{
    dr_drg = (float) r;
    dp_drg = (float) p;
    dy_drg = (float) y;
}

void
Physical::GetAngularDrag(double& r, double& p, double& y)
{
    r = dr_drg;
    p = dp_drg;
    y = dy_drg;
}

void
Physical::GetAngularThrust(double& r, double& p, double& y)
{
    r = 0;
    p = 0;
    y = 0;

    if      (dr_acc > 0.05 * roll_rate)    r =  1;
    else if (dr_acc < -0.05 * roll_rate)   r = -1;
    else if (dr > 0.01 * roll_rate)        r = -1;
    else if (dr < -0.01 * roll_rate)       r =  1;

    if      (dy_acc > 0.05 * yaw_rate)     y =  1;
    else if (dy_acc < -0.05 * yaw_rate)    y = -1;
    else if (dy > 0.01 * yaw_rate)         y = -1;
    else if (dy < -0.01 * yaw_rate)        y =  1;

    if      (dp_acc > 0.05 * pitch_rate)   p =  1;
    else if (dp_acc < -0.05 * pitch_rate)  p = -1;
    else if (dp > 0.01 * pitch_rate)       p = -1;
    else if (dp < -0.01 * pitch_rate)      p =  1;
}


void
Physical::SetPrimary(const Point& l, double m)
{
    primary_loc  = l;
    primary_mass = m;
}

void
Physical::SetGravity(double g)
{
    if (g >= 0)
    g_accel = (float) g;
}

void
Physical::SetBaseDensity(double d)
{
    if (d >= 0)
    Do = (float) d;
}

// +--------------------------------------------------------------------+

void
Physical::InflictDamage(double damage, int /*type*/)
{
    integrity -= (float) damage;

    if (integrity < 1.0f)
    integrity = 0.0f;
}

// +--------------------------------------------------------------------+

int
Physical::CollidesWith(Physical& o)
{
    // representation collision test (will do bounding spheres first):
    if (rep && o.rep)
    return rep->CollidesWith(*o.rep);

    Point delta_loc = Location() - o.Location();

    // bounding spheres test:
    if (delta_loc.length() > radius + o.radius)
    return 0;

    // assume collision:
    return 1;
}


// +--------------------------------------------------------------------+

void
Physical::ElasticCollision(Physical& a, Physical& b)
{
    double mass_sum   = a.mass + b.mass;
    double mass_delta = a.mass - b.mass;

    Point vel_a = (Point(b.velocity) * (2 * b.mass) + Point(a.velocity) * mass_delta) * (1/mass_sum);
    Point vel_b = (Point(a.velocity) * (2 * a.mass) - Point(b.velocity) * mass_delta) * (1/mass_sum);

    a.velocity = vel_a;
    b.velocity = vel_b;
}

// +--------------------------------------------------------------------+

void
Physical::InelasticCollision(Physical& a, Physical& b)
{
    double mass_sum   = a.mass + b.mass;

    Point vel_a = (Point(a.velocity) * a.mass + Point(b.velocity) * b.mass) * (1/mass_sum);

    a.velocity = vel_a;
    b.velocity = vel_a;
}

// +--------------------------------------------------------------------+

void
Physical::SemiElasticCollision(Physical& a, Physical& b)
{
    double mass_sum   = a.mass + b.mass;
    double mass_delta = a.mass - b.mass;

    Point avel  = a.Velocity();
    Point bvel  = b.Velocity();
    Point dv    = avel - bvel;

    // low delta-v: stick
    if (dv.length() < 20) {
        if (a.mass > b.mass) {
            b.velocity = a.velocity;
        }

        else {
            a.velocity = b.velocity;
        }
    }

    // high delta-v: bounce
    else {
        Point Ve_a  = (bvel * (2 * b.mass) + avel * mass_delta) * (1/mass_sum) * 0.65;
        Point Ve_b  = (avel * (2 * a.mass) - bvel * mass_delta) * (1/mass_sum) * 0.65;
        Point Vi_ab = (avel * a.mass + bvel * b.mass)           * (1/mass_sum) * 0.35;

        a.arcade_velocity = Point();
        b.arcade_velocity = Point();

        a.velocity = Ve_a + Vi_ab;
        b.velocity = Ve_b + Vi_ab;
    }
}

